package bancosys.tec.utils.md5sum;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Classe utilitária que calcula o hash de uma string ou arquivo baseado no algoritmo MD5 de criptografia. Tem a função idêntica ao comando
 * unix md5sum.
 * 
 * @author Evaristo
 * @version $Id:$ <b>Created on: </b> 13/03/2008
 */
public final class MD5SumTool {

    private static final int MD5_STRING_SIZE = 32;

    private static final int BYTE_ARRAY_SIZE = 1024;

    private static final String HASH_ALGORITHM = "MD5";

    private static final short MASK_FOUR_BITS = 0xf;

    private static final short FOUR_BITS = 4;

    /**
     * Contrutor privado.
     */
    private MD5SumTool() {
        // Não faz nada.
    }

    /**
     * Retorna o hash MD5 da String passada como parâmetro.
     * 
     * @param string - String para ser lida e calculado o hash MD5
     * @return String - String contendo hash md5 da String passado por parâmetro
     */
    public static String md5Sum(String string)  {
        try {
            return md5Sum(string.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e.getMessage());
        } catch (IOException e) {
            throw new IllegalStateException(e.getMessage());
        }
    }

    /**
     * Retorna o hash MD5 do arquivo passado no parâmetro. O hash MD5 calculado do primeiro byte até o limite contido na variável estática
     * MD5_BYTE_LIMIT da própria classe.
     * 
     * @param file - Arquivo para ser lido e calculado o hash MD5
     * @return String - String contendo hash md5 do arquivo
     * @throws IOException - Erro na leitura e manipulação do arquivo
     */
    public static String md5Sum(File file) throws IOException {
        FileInputStream in = new FileInputStream(file.getAbsolutePath());
        try {
            return md5Sum(in);
        } finally {
            in.close();
        }
    }

    /**
     * Retorna o hash MD5 do array de bytes passado no parâmetro.
     * 
     * @param input - Array de bytes que será a base para o calculo do hash MD5
     * @return String - String contendo hash MD5 do array de bytes passado por parâmetro
     * @throws IOException Erro na leitura e manipulação do arquivo
     */
    public static String md5Sum(byte[] input) throws IOException {
        return md5Sum(input, -1);
    }

    /**
     * Retorna o hash MD5 do array de bytes passado no parâmetro. Os bytes a serem usados no calculo limitado pelo parâmetro limite.
     * 
     * @param input - Array de bytes que será a base para o calculo do hash MD5
     * @param limit - Quantidade de bytes que será usada para o calculo do hash MD5
     * @return String - String contendo hash md5 do array de bytes passado por parâmetro
     * @throws IOException Erro na leitura e manipulação do arquivo
     */
    public static String md5Sum(byte[] input, int limit) throws IOException {
        ByteArrayInputStream is;
        if (limit == -1) {
            is = new ByteArrayInputStream(input);
        } else {
            is = new ByteArrayInputStream(input, 0, limit);
        }
        String md5 = md5Sum(is);
        is.close();
        return md5;
    }

    /**
     * Retorna o hash MD5 do input stream passado no parâmetro. Os bytes a serem usados no calculo limitado pelo parâmetro limite.
     * 
     * @param is - InputStream que será a base para o calculo do hash MD5
     * @return String - String contendo hash md5 do array de bytes passado por parâmetro
     * @throws IOException Erro na leitura e manipulação do arquivo
     */
    public static String md5Sum(InputStream is) throws IOException {
        try {
            MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
            byte[] digest;
            byte[] buffer = new byte[BYTE_ARRAY_SIZE];

            int numRead;
            do {
                numRead = is.read(buffer);
                if (numRead > 0) {
                    md.update(buffer, 0, numRead);
                }
            } while (numRead != -1);
            digest = md.digest();

            char[] array = new char[MD5_STRING_SIZE];
            for (int i = 0; i < digest.length; i++) {
                array[2 * i] = hexDigitLastBytes(digest[i] >>> FOUR_BITS);
                array[1 + 2 * i] = hexDigitLastBytes(digest[i]);
            }

            return new String(array);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e.getMessage());
        }
    }

    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();

    /**
     * Devolve o digito hexadecimal dos ultimos 8 bytes
     * 
     * @param digit int
     * @return char
     */
    private static char hexDigitLastBytes(int digit) {
        return HEX_DIGITS[digit & MASK_FOUR_BITS];
    }

}